﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/


using System;
using Tesla.Math;

namespace Tesla.Graphics {

    /// <summary>
    /// Logic that feeds Engine light values into the shader before a renderable is
    /// to be drawn. This module is designed to work with the engine's default shader
    /// library, although it can be used for custom shaders or replaced outright. An instance
    /// of LightLogic is always created with a Material. A unique LightLogic should only
    /// be associated with one Material at a given time.
    /// 
    /// GlobalAmbient is expected to be a 3-component float vector. LightCount is expected to
    /// be an integer (or float equivalent), and the expected light list is expected to be
    /// composed of the following structure:
    /// 
    /// <code>
    /// //Light struct that represents a Point, Spot, or Directional
    /// //light. Point lights are with a 180 degree inner/outer angle,
    /// //and directional lights have their position's w = 1.0 .
    /// struct Light {
    ///   //Color properties
    ///	  float3 Ambient;
    ///	  float3 Diffuse;
    ///	  float3 Specular;
    ///	
    ///	  //Attenuation properties
    ///	  bool Attenuate;
    ///	  float Constant;
    ///	  float Linear;
    ///	  float Quadratic;
    ///	
    ///   //Positional (in world space) properties
    ///	  float4 Position; //Note: w = 1 means direction is used
    ///	  float3 Direction;
    ///	  float InnerAngle;
    ///	  float OuterAngle;
    /// };
    /// </code>
    /// </summary>
    public class LightLogic : BaseMaterialLogic {
        private String _globalAmbientParamName;
        private bool _globalAmbientSemantic;

        private String _lightListParamName;
        private bool _lightListSemantic;

        private String _lightCountParamName;
        private bool _lightCountSemantic;

        private IEffectParameter _cachedGlobalAmbient;
        private IEffectParameter _cachedLightList;
        private IEffectParameter _cachedLightCount;

        private bool _needsRefresh;

        /// <summary>
        /// Initializes a new instance of the <see cref="LightLogic"/> class with module name "LightLogic".
        /// </summary>
        public LightLogic() : base("LightLogic") {
            _needsRefresh = true;
            BindLightParameter("GLOBALAMBIENT", true, LightParameter.GlobalAmbient);
            BindLightParameter("LIGHTLIST", true, LightParameter.LightList);
            BindLightParameter("LIGHTCOUNT", true, LightParameter.LightCount);
        }

        /// <summary>
        /// Binds an effect parameter to the light parameter.
        /// </summary>
        /// <param name="paramName">Effect parameter name</param>
        /// <param name="lightParam">Light parameter to bind</param>
        public void BindLightParameter(String paramName, LightParameter lightParam) {
            BindLightParameter(paramName, false, lightParam);
        }

        /// <summary>
        /// Binds an effect parameter to the light parameter.
        /// </summary>
        /// <param name="paramName">Effect parameter name</param>
        /// <param name="isSemantic">True if the name is a semantic (rather than variable name), false otherwise</param>
        /// <param name="lightParam">Light parameter to bind</param>
        public void BindLightParameter(String paramName, bool isSemantic, LightParameter lightParam) {
            switch(lightParam) {
                case LightParameter.GlobalAmbient:
                    _globalAmbientParamName = paramName;
                    _globalAmbientSemantic = isSemantic;
                    _needsRefresh = true;
                    break;
                case LightParameter.LightCount:
                    _lightCountParamName = paramName;
                    _lightCountSemantic = isSemantic;
                    _needsRefresh = true;
                    break;
                case LightParameter.LightList:
                    _lightListParamName = paramName;
                    _lightListSemantic = isSemantic;
                    _needsRefresh = true;
                    break;
            }
        }

        /// <summary>
        /// Clears a binding.
        /// </summary>
        /// <param name="paramName">Effect parameter name or semantic</param>
        /// <param name="lightParam">LightParameter that the effect parameter is bound to</param>
        public void UnBindLightParameter(String paramName, LightParameter lightParam) {
            switch(lightParam) {
                case LightParameter.GlobalAmbient:
                    _globalAmbientParamName = null;
                    _globalAmbientSemantic = false;
                    _cachedGlobalAmbient = null;
                    break;
                case LightParameter.LightCount:
                    _lightCountParamName = null;
                    _lightCountSemantic = false;
                    _cachedLightCount = null;
                    break;
                case LightParameter.LightList:
                    _lightListParamName = null;
                    _lightListSemantic = false;
                    _cachedLightList = null;
                    break;
            }
        }

        /// <summary>
        /// Execute the logic for the given material and renderable (usually a Mesh).
        /// </summary>
        /// <param name="material">Material that is executing the logic.</param>
        /// <param name="renderer">Renderer used for drawing</param>
        /// <param name="renderable">Renderable that owns the Material</param>
        public override void Execute(Material material, IRenderer renderer, IRenderable renderable) {
            if(renderable == null || renderer == null || material == null) {
                return;
            }

            if(IsEnabled) {
                if(_needsRefresh) {
                    CacheParameters(material.Effect);
                }

                //We assume this comes to us already sorted.
                ILightCollection lights = renderable.WorldLights;

                //Return if the collection doesn't need to update us or there are no lights to update with
                if(!lights.RefreshShader || lights.LightCount == 0) {
                    //Set to zero
                    if(_cachedLightCount != null) {
                        _cachedLightCount.SetValue(0);
                    }
                    return;
                }

                //Tell the collection not to update us until it changes
                lights.RefreshShader = false;

                //Set the global ambient value
                if(_cachedGlobalAmbient != null) {
                    _cachedGlobalAmbient.SetValue(lights.GlobalAmbient.ToVector3());
                }

                //Update the actual light list
                if(_cachedLightList != null) {

                    //If we're dealing with a single parameter and not an array...
                    if(_cachedLightList.Elements.Count == 0) {
                        SetLight(lights[0], _cachedLightList);
                        return;
                    }
                    
                    //Ensure the max light counts match, if the element count is less than
                    //the collection's max lights, use that, otherwise use the collection's
                    int numLights = 0;
                    if(lights.LightCount <= _cachedLightList.Elements.Count) {
                        numLights = lights.LightCount;
                    } else if(lights.LightCount > _cachedLightList.Elements.Count) {
                        numLights = _cachedLightList.Elements.Count;
                    }

                    //Set the number of lights
                    if(_cachedLightCount != null) {
                        _cachedLightCount.SetValue(numLights);
                    }

                    //Go through each light, and update it to the shader
                    for(int i = 0; i < numLights; i++) {
                        Light light = lights[i];
                        IEffectParameter lParam = _cachedLightList.Elements[i];
                        SetLight(light, lParam);
                    }
                }
            }
        }

        private void SetLight(Light light, IEffectParameter lParam) {
            if(light.LightType == LightType.Directional) {
                DirectionalLight dl = light as DirectionalLight;
                lParam.StructureMembers[0].SetValue(dl.Ambient.ToVector3());
                lParam.StructureMembers[1].SetValue(dl.Diffuse.ToVector3());
                lParam.StructureMembers[2].SetValue(dl.Specular.ToVector3());
                lParam.StructureMembers[3].SetValue(false);
                lParam.StructureMembers[4].SetValue(dl.Constant);
                lParam.StructureMembers[5].SetValue(dl.Linear);
                lParam.StructureMembers[6].SetValue(dl.Quadratic);
                lParam.StructureMembers[7].SetValue(new Vector4(0, 0, 0, 1));
                lParam.StructureMembers[8].SetValue(dl.Direction);
                lParam.StructureMembers[9].SetValue(0f);
                lParam.StructureMembers[10].SetValue(0f);
            } else if(light.LightType == LightType.Point) {
                PointLight pl = light as PointLight;
                lParam.StructureMembers[0].SetValue(pl.Ambient.ToVector3());
                lParam.StructureMembers[1].SetValue(pl.Diffuse.ToVector3());
                lParam.StructureMembers[2].SetValue(pl.Specular.ToVector3());
                lParam.StructureMembers[3].SetValue(pl.Attenuate);
                lParam.StructureMembers[4].SetValue(pl.Constant);
                lParam.StructureMembers[5].SetValue(pl.Linear);
                lParam.StructureMembers[6].SetValue(pl.Quadratic);
                lParam.StructureMembers[7].SetValue(new Vector4(pl.Position, 0));
                lParam.StructureMembers[8].SetValue(Vector3.Down);
                lParam.StructureMembers[9].SetValue(0f);
                lParam.StructureMembers[10].SetValue(0f);
            } else if(light.LightType == LightType.Spot) {
                SpotLight sl = light as SpotLight;
                lParam.StructureMembers[0].SetValue(sl.Ambient.ToVector3());
                lParam.StructureMembers[1].SetValue(sl.Diffuse.ToVector3());
                lParam.StructureMembers[2].SetValue(sl.Specular.ToVector3());
                lParam.StructureMembers[3].SetValue(sl.Attenuate);
                lParam.StructureMembers[4].SetValue(sl.Constant);
                lParam.StructureMembers[5].SetValue(sl.Linear);
                lParam.StructureMembers[6].SetValue(sl.Quadratic);
                lParam.StructureMembers[7].SetValue(new Vector4(sl.Position, 0));
                lParam.StructureMembers[8].SetValue(sl.Direction);
                lParam.StructureMembers[9].SetValue(1 - (sl.InnerAngle / 180f));
                lParam.StructureMembers[10].SetValue(1 - (sl.OuterAngle / 180f));
            }
        }

        private void CacheParameters(Effect e) {
            if(_globalAmbientParamName != null) {
                if(_globalAmbientSemantic) {
                    _cachedGlobalAmbient = e.Parameters.GetParameterBySemantic(_globalAmbientParamName);
                } else {
                    _cachedGlobalAmbient = e.Parameters[_globalAmbientParamName];
                }
            }

            if(_lightCountParamName != null) {
                if(_lightCountSemantic) {
                    _cachedLightCount = e.Parameters.GetParameterBySemantic(_lightCountParamName);
                } else {
                    _cachedLightCount = e.Parameters[_lightCountParamName];
                }
            }

            if(_lightListParamName != null) {
                if(_lightListSemantic) {
                    _cachedLightList = e.Parameters.GetParameterBySemantic(_lightListParamName);
                } else {
                    _cachedLightList = e.Parameters[_lightListParamName];
                }
            }
            _needsRefresh = false;
        }

        /// <summary>
        /// Clones the logic.
        /// </summary>
        /// <returns>Cloned logic</returns>
        public override IMaterialLogic Clone() {
            LightLogic clone = new LightLogic();
            clone.BindLightParameter(_globalAmbientParamName, _globalAmbientSemantic, LightParameter.GlobalAmbient);
            clone.BindLightParameter(_lightListParamName, _lightListSemantic, LightParameter.LightList);
            clone.BindLightParameter(_lightCountParamName, _lightCountSemantic, LightParameter.LightCount);
            clone.IsEnabled = IsEnabled;
            return clone;
        }
    }
}
